﻿namespace SenenFernandez.SmartExtensionMethods.Interactivity
{
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Reflection;
    using System.Windows;

    /// <summary>
    /// Calls a method on a specified object when invoked.
    /// </summary>
    public class CallMethodAction : TriggerAction<DependencyObject>
    {
        public static readonly DependencyProperty TargetObjectProperty =
            DependencyProperty.Register("TargetObject", typeof (object), typeof (CallMethodAction),
                new PropertyMetadata(OnTargetObjectChanged));

        public static readonly DependencyProperty MethodNameProperty =
            DependencyProperty.Register("MethodName", typeof (string), typeof (CallMethodAction),
                new PropertyMetadata(OnMethodNameChanged));

        private List<MethodDescriptor> methodDescriptors;

        /// <summary>
        /// The object that exposes the method of interest. 
        /// This is a dependency property.
        /// </summary>
        public object TargetObject
        {
            get { return base.GetValue(TargetObjectProperty); }
            set { base.SetValue(TargetObjectProperty, value); }
        }

        /// <summary>
        /// The name of the method to invoke. 
        /// This is a dependency property.
        /// </summary>
        public string MethodName
        {
            get { return (string) base.GetValue(MethodNameProperty); }
            set { base.SetValue(MethodNameProperty, value); }
        }

        /// <summary>
        /// Initializes a new instance of the CallMethodAction class.
        /// </summary>
        public CallMethodAction()
        {
            methodDescriptors = new List<MethodDescriptor>();
        }

        /// <summary>
        /// Invokes the action.
        /// </summary>
        protected override void Invoke(object parameter)
        {
            if (base.AssociatedObject != null)
            {
                MethodDescriptor methodDescriptor = FindBestMethod(parameter);
                if (methodDescriptor != null)
                {
                    ParameterInfo[] parameters = methodDescriptor.Parameters;
                    if (parameters.Length == 0)
                    {
                        methodDescriptor.MethodInfo.Invoke(Target, null);
                        return;
                    }
                    if (parameters.Length == 2 && base.AssociatedObject != null && parameter != null &&
                        parameters[0].ParameterType.IsAssignableFrom(base.AssociatedObject.GetType()) &&
                        parameters[1].ParameterType.IsAssignableFrom(parameter.GetType()))
                    {
                        methodDescriptor.MethodInfo.Invoke(Target, new object[]
                        {
                            base.AssociatedObject,
                            parameter
                        });
                        return;
                    }
                }
                else
                {
                    if (TargetObject != null)
                    {
                        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                            "ExceptionStringTable.CallMethodActionValidMethodNotFoundExceptionMessage",
                            new object[]
                            {
                                MethodName,
                                TargetObject.GetType().Name
                            }));
                    }
                }
            }
        }

        /// <summary>
        /// Called after the action is attached to an AssociatedObject.
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();
            UpdateMethodInfo();
        }

        /// <summary>
        /// Called when the action is being detached from its AssociatedObject, but before it has actually occurred.
        /// </summary>
        protected override void OnDetaching()
        {
            methodDescriptors.Clear();
            base.OnDetaching();
        }

        private object Target
        {
            get { return TargetObject ?? base.AssociatedObject; }
        }

        private MethodDescriptor FindBestMethod(object parameter)
        {
            return methodDescriptors
                .FirstOrDefault(
                    methodDescriptor =>
                        !methodDescriptor.HasParameters ||
                        (parameter != null && methodDescriptor.SecondParameterType.IsAssignableFrom(parameter.GetType())));
        }

        private void UpdateMethodInfo()
        {
            methodDescriptors.Clear();
            if (Target != null && !string.IsNullOrEmpty(MethodName))
            {
                var type = Target.GetType();
                var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public);
                foreach (var methodInfo in methods)
                {
                    if (IsMethodValid(methodInfo))
                    {
                        var parameters = methodInfo.GetParameters();
                        if (AreMethodParamsValid(parameters))
                        {
                            methodDescriptors.Add(new MethodDescriptor(methodInfo, parameters));
                        }
                    }
                }
                methodDescriptors =
                    methodDescriptors.OrderByDescending(
                        delegate(MethodDescriptor methodDescriptor)
                        {
                            int num = 0;
                            if (methodDescriptor.HasParameters)
                            {
                                Type type2 = methodDescriptor.SecondParameterType;
                                while (type2 != typeof (EventArgs))
                                {
                                    num++;
                                    type2 = type2.BaseType;
                                }
                            }
                            return methodDescriptor.ParameterCount + num;
                        }
                        ).ToList();
            }
        }

        private bool IsMethodValid(MethodInfo method)
        {
            return string.Equals(method.Name, MethodName, StringComparison.Ordinal) &&
                   !(method.ReturnType != typeof (void));
        }

        private static bool AreMethodParamsValid(ParameterInfo[] methodParams)
        {
            if (methodParams.Length == 2)
            {
                if (methodParams[0].ParameterType != typeof (object))
                {
                    return false;
                }
                if (!typeof (EventArgs).IsAssignableFrom(methodParams[1].ParameterType))
                {
                    return false;
                }
            }
            else
            {
                if (methodParams.Length != 0)
                {
                    return false;
                }
            }
            return true;
        }

        private static void OnMethodNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var callMethodAction = sender as CallMethodAction;
            if (callMethodAction != null)
            {
                callMethodAction.UpdateMethodInfo();
            }
        }

        private static void OnTargetObjectChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
        {
            var callMethodAction = sender as CallMethodAction;
            if (callMethodAction != null)
            {
                callMethodAction.UpdateMethodInfo();
            }
        }

        private class MethodDescriptor
        {
            public MethodInfo MethodInfo { get; private set; }

            public bool HasParameters
            {
                get { return Parameters.Length > 0; }
            }

            public int ParameterCount
            {
                get { return Parameters.Length; }
            }

            public ParameterInfo[] Parameters { get; private set; }

            public Type SecondParameterType
            {
                get
                {
                    if (Parameters.Length >= 2)
                    {
                        return Parameters[1].ParameterType;
                    }
                    return null;
                }
            }

            public MethodDescriptor(MethodInfo methodInfo, ParameterInfo[] methodParams)
            {
                MethodInfo = methodInfo;
                Parameters = methodParams;
            }
        }
    }
}